package me.spring.cloud.common.components.ips.maker;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import me.spring.cloud.common.components.ips.common.DataBlock;
import me.spring.cloud.common.components.ips.common.DbConfig;
import me.spring.cloud.common.components.ips.common.DbMakerConfigException;
import me.spring.cloud.common.components.ips.common.HeaderBlock;
import me.spring.cloud.common.components.ips.common.IndexBlock;
import me.spring.cloud.common.components.ips.common.IpUtil;

/**
 * fast ip db maker
 *
 * db struct:
 * 1. header part
 * 1): super part:
 * +------------+-----------+
 * | 4 bytes	| 4 bytes   |
 * +------------+-----------+
 *  start index ptr, end index ptr
 *
 * 2): b-tree index part
 * +------------+-----------+-----------+-----------+
 * | 4bytes		| 4bytes	| 4bytes	| 4bytes	| ...
 * +------------+-----------+-----------+-----------+
 *  start ip ptr  index ptr
 *
 * 2. data part:
 * +------------+-----------------------+
 * | 2bytes		| dynamic length		|
 * +------------+-----------------------+
 *  data length   city_id|Country|Province|Area|City|ISP
 *
 * 3. index part: (ip range)
 * +------------+-----------+---------------+
 * | 4bytes		| 4bytes	| 4bytes		|
 * +------------+-----------+---------------+
 *  start ip 	  end ip	  3 byte data ptr & 1 byte data length
 */
public class Txt2DbMaker {

  /**
   * db config
   */
  private DbConfig dbConfig;

  /**
   * ip source file path
   */
  private File ipSrcFile;

  /**
   * buffer
   */
  private LinkedList<IndexBlock> indexPool;
  private LinkedList<HeaderBlock> headerPool;

  /**
   * region and data ptr mapping data
   */
  private HashMap<String, DataBlock> regionPtrPool = null;

  /**
   * construct method
   *
   * @param  config
   * @param  ipSrcFile tb source ip file
   * @throws DbMakerConfigException
   * @throws IOException
   */
  public Txt2DbMaker(DbConfig config, String ipSrcFile) throws IOException {
    this.dbConfig = config;
    this.ipSrcFile = new File(ipSrcFile);
    this.regionPtrPool = new HashMap<>();
    if (this.ipSrcFile.exists() == false) {
      throw new IOException("Error: Invalid file path " + ipSrcFile);
    }
  }

  /**
   * make the Db file
   *
   * @throws IOException
   * @param  dbFile target output file path
   */
  public void make(String dbFile) throws IOException {
    //alloc the header size
    BufferedReader reader = new BufferedReader(new FileReader(this.ipSrcFile));
    RandomAccessFile raf = new RandomAccessFile(dbFile, "rw");

    //init the db file
    initDbFile(raf);
    System.out.println("+-Db file initialized.");

    //analysis main loop
    System.out.println("+-Try to write the data blocks ... ");
    String line = null;
    while ((line = reader.readLine()) != null) {
      line = line.trim();
      if (line.length() == 0) {
        continue;
      }
      if (line.charAt(0) == '#') {
        continue;
      }

      //1. get the start ip
      int sIdx = 0, eIdx = 0;
      if ((eIdx = line.indexOf('|', sIdx + 1)) == -1) {
        continue;
      }
      String startIp = line.substring(sIdx, eIdx);

      //2. get the end ip
      sIdx = eIdx + 1;
      if ((eIdx = line.indexOf('|', sIdx + 1)) == -1) {
        continue;
      }
      String endIp = line.substring(sIdx, eIdx);

      //3. get the region cityId
      sIdx = eIdx + 1;
      if ((eIdx = line.indexOf('|', sIdx + 1)) == -1) {
        continue;
      }
      String cityId = line.substring(sIdx, eIdx);

      //3. get the region
      sIdx = eIdx + 1;
      String region = line.substring(sIdx);

      System.out.println("+-Try to process item " + line);
      addDataBlock(raf, startIp, endIp, cityId, region);
      System.out.println("|--[Ok]");
    }
    System.out.println("|--Data block flushed!");
    System.out.println("|--Data file pointer: " + raf.getFilePointer() + "\n");

    //write the index bytes
    System.out.println("+-Try to write index blocks ... ");

    //record the start block
    IndexBlock indexBlock = null;
    HeaderBlock hb = null;
    indexBlock = indexPool.getFirst();
    long indexStartIp = indexBlock.getStartIp(),
        indexStratPtr = raf.getFilePointer(), indexEndPtr;
    headerPool.add(new HeaderBlock(indexStartIp, (int) (indexStratPtr)));

    int blockLength = IndexBlock.getIndexBlockLength();
    int counter = 0, shotCounter = (dbConfig.getIndexBlockSize() / blockLength) - 1;
    Iterator<IndexBlock> indexIt = indexPool.iterator();
    while (indexIt.hasNext()) {
      indexBlock = indexIt.next();
      if (++counter >= shotCounter) {
        hb = new HeaderBlock(
            indexBlock.getStartIp(),
            (int) raf.getFilePointer()
        );

        headerPool.add(hb);
        counter = 0;
      }

      //write the buffer
      raf.write(indexBlock.getBytes());
    }

    //record the end block
    if (counter > 0) {
      indexBlock = indexPool.getLast();
      hb = new HeaderBlock(
          indexBlock.getStartIp(),
          ((int) raf.getFilePointer()) - IndexBlock.getIndexBlockLength()
      );

      headerPool.add(hb);
    }

    indexEndPtr = raf.getFilePointer();
    System.out.println("|--[Ok]");

    //write the super blocks
    System.out.println("+-Try to write the super blocks ... ");
    //reset the file pointer
    raf.seek(0L);
    byte[] superBuffer = new byte[8];
    IpUtil.writeIntLong(superBuffer, 0, indexStratPtr);
    IpUtil.writeIntLong(superBuffer, 4, indexEndPtr - blockLength);
    raf.write(superBuffer);
    System.out.println("|--[Ok]");

    //write the header blocks
    System.out.println("+-Try to write the header blocks ... ");
    Iterator<HeaderBlock> headerIt = headerPool.iterator();
    while (headerIt.hasNext()) {
      HeaderBlock headerBlock = headerIt.next();
      raf.write(headerBlock.getBytes());
    }

    //write the copyright and the release timestamp info
    System.out.println("+-Try to write the copyright and release date info ... ");
    raf.seek(raf.length());
    Calendar cal = Calendar.getInstance();
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
    String copyright = "Created by lionsoul at " + dateFormat.format(cal.getTime());
    raf.write((int) (cal.getTime().getTime() / 1000));  //the unix timestamp
    raf.write(copyright.getBytes());
    System.out.println("|--[Ok]");

    reader.close();
    raf.close();
  }

  /**
   * initialize the db file
   * @throws IOException
   * @param  raf
   */
  private void initDbFile(RandomAccessFile raf) throws IOException {
    //1. zero fill the header part
    raf.seek(0L);
    //super block
    raf.write(new byte[8]);
    //header block
    raf.write(new byte[dbConfig.getTotalHeaderSize()]);
    headerPool = new LinkedList<>();
    indexPool = new LinkedList<>();
  }

  /**
   * internal method to add a new data block record
   *
   * @param  raf
   * @param  startIp
   * @param  endIp
   * @param  region data
   */
  private void addDataBlock(RandomAccessFile raf, String startIp, String endIp, String cityId, String region) {
    try {
      byte[] data = region.getBytes("UTF-8");
      int dataPtr = 0;
      //check region ptr pool first
      if (regionPtrPool.containsKey(region)) {
        DataBlock dataBlock = regionPtrPool.get(region);
        dataPtr = dataBlock.getDataPtr();
        System.out.println("dataPtr: " + dataPtr + ", region: " + region);
      } else {
        byte[] city = new byte[4];
        int city_id = Integer.parseInt(cityId);
        IpUtil.writeIntLong(city, 0, city_id);
        dataPtr = (int) raf.getFilePointer();
        raf.write(city);
        raf.write(data);
        regionPtrPool.put(region, new DataBlock(city_id, region, dataPtr));
      }
      //add the data index blocks
      IndexBlock ib = new IndexBlock(
          IpUtil.ip2long(startIp), IpUtil.ip2long(endIp), dataPtr,data.length + 4);
      indexPool.add(ib);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public DbConfig getDbConfig() {
    return dbConfig;
  }

  public Txt2DbMaker setDbConfig(DbConfig dbConfig) {
    this.dbConfig = dbConfig;
    return this;
  }

  public File getIpSrcFile() {
    return ipSrcFile;
  }

  public Txt2DbMaker setIpSrcFile(File ipSrcFile) {
    this.ipSrcFile = ipSrcFile;
    return this;
  }
}